Skip to content

fix(security): refuse pickle on object-dtype decode by default (CWE-502)#58

Open
willardjansen wants to merge 1 commit into
lebedov:masterfrom
willardjansen:fix/cwe-502-default-deny-pickle
Open

fix(security): refuse pickle on object-dtype decode by default (CWE-502)#58
willardjansen wants to merge 1 commit into
lebedov:masterfrom
willardjansen:fix/cwe-502-default-deny-pickle

Conversation

@willardjansen
Copy link
Copy Markdown

Summary

decode() calls pickle.loads() with zero validation whenever a msgpack payload sets kind=b'O', so any caller of unpackb() / unpack() / Unpacker will execute arbitrary code embedded in the pickle stream. The pickle reduce protocol lets a 122-byte crafted .msgpack file invoke os.system, subprocess.Popen, etc. This is the unfixed vulnerability documented in #57 and partially addressed by the unmerged #52.

This PR gates the pickle path on an explicit allow_pickle kwarg, matching numpy's own np.load(allow_pickle=...) convention:

Value Behavior
False (new default) Raises ValueError when kind=b'O' is encountered
'restricted' RestrictedUnpickler that allowlists numpy reconstruction primitives + safe Python builtins and blocks known pickle-RCE gadgets (eval, exec, getattr, __import__, …)
True Legacy pickle.loads — for fully trusted sources only

The kwarg is threaded through decode(), all three Unpacker.__init__ branches (msgpack < 0.4 / < 1.0 / ≥ 1.0), unpack(), and unpackb(). pack() / packb() accept and discard it for symmetry.

Compatibility

This is a breaking change for callers that round-trip object-dtype ndarrays through unpackb() without specifying allow_pickle. The fix is one keyword:

arr = msgpack.unpackb(packed, allow_pickle='restricted')

Existing object-dtype callers can migrate either to 'restricted' (recommended) or to True (explicit opt-in to the legacy unrestricted path).

Tests

The existing test_numpy_array_object opts in via allow_pickle='restricted'. Five new tests cover:

  • Default refusal raises ValueError with a message naming allow_pickle
  • allow_pickle=True restores legacy round-trip behavior
  • Hand-crafted os.system __reduce__ payload is refused with the default
  • Same payload is refused by the restricted unpickler with pickle.UnpicklingError
  • builtins.eval gadget is on the explicit block list
  • Bad allow_pickle values raise ValueError

All 36 tests pass locally on Python 3.13 / numpy 2.4.2 / msgpack 1.1.1.

Disclosure

Filed via huntr.com on 2026-04-12 and validated by huntr triage on 2026-05-29 (CWE-502, CVSS 8.8). This PR is the proposed coordinated-disclosure fix.

Closes #57. Supersedes #52 (extends with restricted-unpickler opt-in).

decode() called pickle.loads() with zero validation on attacker-controlled
data whenever a payload set kind=b'O'. Any caller of unpackb() / unpack() /
Unpacker was exposed to arbitrary code execution from a 122-byte crafted
.msgpack file — the pickle reduce protocol allows attacker payloads to
invoke os.system, subprocess.Popen, etc.

This commit gates the pickle path on an explicit allow_pickle kwarg:

  * allow_pickle=False  (new default) — raises ValueError on kind=b'O'
  * allow_pickle='restricted'         — RestrictedUnpickler that
                                         allowlists numpy reconstruction
                                         primitives + safe Python builtins
                                         and blocks known pickle-RCE
                                         gadgets (eval/exec/getattr/...)
  * allow_pickle=True                 — legacy pickle.loads (use only with
                                         trusted sources)

Threaded through decode(), Unpacker (all three msgpack-version branches),
unpack(), and unpackb(). pack() / packb() accept and ignore the kwarg for
symmetry.

The existing test_numpy_array_object now opts in via
allow_pickle='restricted'. Five new tests cover:

  - default refusal raises ValueError
  - allow_pickle=True restores legacy behavior
  - hand-crafted os.system __reduce__ payload is refused by default
  - same payload is refused by the restricted unpickler
  - builtins.eval gadget is on the explicit block list
  - bad allow_pickle values raise ValueError

Fixes the unfixed CWE-502 documented in issue lebedov#57 and extends PR lebedov#52
with a restricted-unpickler opt-in mode.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: Arbitrary Code Execution via pickle.loads in decode() when kind='O'

1 participant